/* CSci4061 F2012 Assignment 4
 * date: 12/1/12
 * name: Jeff Goin, Jack Jones
 * id: 2999885, 4220764 */

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <curses.h>
#include <string.h>
#include "textbuff.h"

#define CTRL(c) ((c) & 037)

#define min(X, Y)                \
     ({ typeof (X) x_ = (X);          \
        typeof (Y) y_ = (Y);          \
        (x_ < y_) ? x_ : y_; })


// Defined constants to be used with the message data-structure
#define EDIT 0
#define SAVE 1
#define QUIT 2
#define DEL  3

// Maximum length of the message passing queue
#define QUEUEMAX 20


// Data structure for message passing queue used to communicate
// between the router and UI thread
struct message_t{
    int data;
    int row;
    int col;
    int command;
    struct message_t *next;
} typedef message;


// mutex to control access to the text buffer
pthread_mutex_t text_ = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_t save_ = PTHREAD_MUTEX_INITIALIZER;

// mutex to control access to the message queue
pthread_mutex_t queue_ = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t queue_full = PTHREAD_COND_INITIALIZER;
pthread_cond_t queue_empty = PTHREAD_COND_INITIALIZER;


// current queue length and pointers
int queue_len = 0;
message *head = NULL;
message *tail = NULL;

char *temp_file;

// The current position of the cursor in the screen
int row;
int col;

// Lines visible in the current view of textbuff
// visible on the screen
int view_min;
int view_max;

/*
 * Removes the first message from the message queue
 * and returns it.
 */
message* pop(){
    pthread_mutex_lock(&queue_);
    while(queue_len == 0)
        pthread_cond_wait(&queue_full, &queue_);
    
    message *temp, *old_head;
    if (head == NULL){
        printf("Queue is empty\n");
        return NULL;
    }
    else {
        temp = head;
        //old_head = head;
    }
    if (head == tail) {
        head = NULL;
        tail = NULL;
    }
    else {
        head = head->next;
    }
    queue_len--;

    pthread_cond_signal(&queue_empty);
    pthread_mutex_unlock(&queue_);
    return temp;
}

/*
 * Inserts a message at the back of the message queue
 */
void push(message* m_){
    pthread_mutex_lock(&queue_);
    while(queue_len == QUEUEMAX)
        pthread_cond_wait(&queue_empty, &queue_);
    
    if (head == NULL){
        head = m_;
        head->next = NULL;
        tail = head;
    }
    else {
        tail->next = m_;
        tail->next = tail;
    }
    queue_len++;
    
    pthread_cond_signal(&queue_full);
    pthread_mutex_unlock(&queue_);
}


/*
 * Redraws the screen with lines min_line -> max_line visible on the screen
 * places the cursor at (r_, c_) when done. If insert_ is 1 then "INPUT MODE"
 * text is displayed otherwise if 0 it isn't.
 */
int redraw(int min_line, int max_line,int r_, int c_, int insert_){
    erase();
    if(max_line - min_line != LINES-1){
        perror("HELP");
        pthread_exit(NULL);
    }
    move(0,0);

    pthread_mutex_lock(&text_);

    for(;min_line < max_line;min_line++){
        char *line;
        if(getLine(min_line,&line) == 0)
            break;
        int j;
        for(j=0;j < strlen(line);j++){
            addch(*(line+j));
        }
        addch('\n');
    }

    pthread_mutex_unlock(&text_);

    if(insert_){
        standout();
        mvaddstr(LINES-1, COLS-20, "INPUT MODE");
        standend();
    }

    move(r_,c_);
    refresh();
    return 1;
}


/*
 * Input loop of the UI Thread;
 * Loops reading in characters using getch() and placing them into the textbuffer using message queue interface
 */
void input_mode(){
    int c;
    redraw(view_min, view_max, row, col, 1);
    refresh();
    for(;;){
        c = getch();
        if(c == CTRL('D')){
            break;
        }

        //Add code here to insert c into textbuff at (row, col) using the message queue interface.
        message edit_msg;
        edit_msg.data = c;
        edit_msg.row = row;
        edit_msg.col = col;
        edit_msg.command = EDIT;
        push(&edit_msg);
        // ------------------------------
        if(col<COLS-1){
            col++;
        }else{
            col = 0;
            row++;
        }
        if(row > LINES - 1){
            view_min++;
            view_max++;
        }
        redraw(view_min,view_max,row,col,1);
    }
    redraw(view_min,view_max,row,col,0);
}


/*
 * Main loop of the UI thread. It reads in commands as characters
 */
void loop(){
    int c;

    while(1){
        move(row,col);
        refresh();
        c = getch();

        switch(c){
            case 'h':
            case KEY_LEFT:
                if(col > 0)
                    col--;
                else
                    flash();
                break;
            case 'j':
            case KEY_DOWN:
                if(row < LINES -2)
                    row++;
                else
                    if(view_max+1<=getLineLength())
                        redraw(++view_min,++view_max,row,col,0);
                    else
                        flash();
                break;
            case 'k':
            case KEY_UP:
                if(row > 0)
                    row--;
                else
                    if(view_min-1 > -1)
                        redraw(--view_min,--view_max,row,col,0);
                    else
                        flash();
                break;
            case 'l':
            case KEY_RIGHT:
                if (col < COLS - 1)
                    col++;
                else
                    flash();
                break;
            case 'i':
            case KEY_IC:
                input_mode();
                break;
            case 'x':
                flash();

                // Add code here to delete character (row, col) from textbuf
                message del_msg;
                del_msg.row = row;
                del_msg.col = col;
                del_msg.command = DEL;
                push(&del_msg);
                // ------------------------------

                redraw(view_min,view_max,row,col,0);
                break;
            case 'w':
                flash();

                // Add code here to save the textbuf file
                message save_msg;
                save_msg.command = SAVE;
                push(&save_msg);
                // ------------------------------

                break;
            case 'q':
                endwin();

                // Add code here to quit the program
                message quit_msg;
                quit_msg.command = QUIT;
                push(&quit_msg);
                pthread_exit(NULL);
                // ------------------------------
            default:
                flash();
                break;
        }

    }
}


/*
 * Function to be used to spawn the UI thread.
 */
void *start_UI(void *threadid){

    initscr();
    cbreak();
    nonl();
    noecho();
    idlok(stdscr, TRUE);
    keypad(stdscr,TRUE);

    view_min = 0;
    view_max = LINES-1;

    redraw(view_min,view_max,row,col,0);

    refresh();
    loop();
}

/*
 * Function to be used to spawn the autosave thread.
 */
void *autosave(void *threadid){
    char *tempchar = "~";
    strcat(temp_file, tempchar);

    // This function loops until told otherwise from the router thread. Each loop:
    while(1){
        pthread_mutex_lock(&save_);
        // Open the temporary file
        FILE *f = fopen(temp_file, "w");
        if (f == NULL)
	    exit(0);
	// Read lines from the text buffer and save them to the temporary file
        char *line;
        int i = 0;
        pthread_mutex_lock(&text_);
        int num_line = getLineLength();

        while (i < num_line){
	    int j = getLine(i, &line);
	    if (j == 0){
	        perror("Failed to get line");
		exit(0);
	    }
	    fprintf(f, "%s\n", line);
            i++;
        }
        pthread_mutex_unlock(&text_);
        // Close the temporary file and sleep for 5 sec.
        fclose(f);
        pthread_mutex_unlock(&save_);
        sleep(5);
    }
}

int main(int argc, char **argv){

    row = 0;
    col = 0;

    if (argc != 2){
        perror("File not specified");
        exit(1);
    }
    // get text file from argv
    char* file = argv[1];
    temp_file = file;
	
    // set up necessary data structures for message passing queue and initialize textbuff
    init_textbuff(file);

    // spawn UI thread
    pthread_t ui_thread;  
    pthread_create(&ui_thread, NULL, start_UI, (void *) ui_thread);

    // spawn auto-save thread
    pthread_t autosave_thread;
    pthread_create(&autosave_thread, NULL, autosave, (void *) autosave_thread);

    int quit = 0;
    // Main loop until told otherwise
    while(quit == 0){
        // Recieve messages from the message queue
        message *msg;
        msg = pop();
        
        // If EDIT then place the edits into the text buffer
        if (msg->command == EDIT){
            pthread_mutex_lock(&text_);
            insert(msg->row, msg->col, msg->data);
            pthread_mutex_unlock(&text_);
        }
        // If SAVE then save the file additionally delete the temporary save
        if (msg->command == SAVE){
            pthread_mutex_lock(&save_);
	    if (file[strlen(file) - 1] == '~')
	        file[strlen(file) - 1] = '\0';
            FILE *fout = fopen(file, "w");
            if (fout == NULL) {
	        exit(0);
	    }    
            char *line;
            int i = 0;
            pthread_mutex_lock(&text_);
            int num_lines = getLineLength();

            while (i < num_lines){
	        int j = getLine(i, &line);
		if (j == 0){
		    perror("Failed to get line");
		    exit(0);
		}
		fprintf(fout, "%s\n", line);
		i++;
	    }
	    
            pthread_mutex_unlock(&text_);

	    char *tempchar = "~";
	    strcat(temp_file, tempchar);

	    if (remove(temp_file) != 0)
	        exit(0);
            fclose(fout);
            pthread_mutex_unlock(&save_);
        }
        // If QUIT then quit the program and tell the appropriate threads to stop
        if (msg->command == QUIT){
            quit = 1;
        }
        // If DEL then delete the specified character from the text buffer
        if (msg->command == DEL){
            pthread_mutex_lock(&text_);
            deleteCharacter(msg->row, msg->col);
            pthread_mutex_unlock(&text_);
        }
    }

    pthread_cancel(autosave_thread);
    pthread_join(autosave_thread, NULL);
    pthread_join(ui_thread, NULL);

    // Clean up data structures

    // Destroy condition variables and locks
    pthread_cond_destroy(&queue_empty);
    pthread_cond_destroy(&queue_full);
    pthread_mutex_destroy(&text_);
    pthread_mutex_destroy(&save_);
    pthread_mutex_destroy(&queue_);

    deleteBuffer();

    return 0;
}
